using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.SqlServer.Dts.Pipeline;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;

namespace SSISBatchGeocoder
{
    /// <summary>
    /// This class represents the asynchronous SSIS transformation component for batch geocoding of address data.
    /// </summary>
    [DtsPipelineComponent(DisplayName = "SSIS Batch Geocoder", ComponentType = ComponentType.Transform)]
    public class GeocoderTransformation : PipelineComponent
    {
        private enum CustomPropertiesBase { Literal = 0, Variables = 1, InputColumns = 2, MinConfidence = 3, TrueFalseMetadataChange = 4 };        // used internally by the helper function AddCustomProperty to add custom properties

        #region Constants
        private const string OUTPUT_COLUMN_NAME_ENTITY_TYPE = "EntityType";                         // name of the EntityType output column
        private const string OUTPUT_COLUMN_NAME_ROOFTOP_LATITUDE = "RooftopLatitude";               // name of the RooftopLatitude output column
        private const string OUTPUT_COLUMN_NAME_ROOFTOP_LONGITUDE = "RooftopLongitude";             // name of the RooftopLongitude output column
        private const string OUTPUT_COLUMN_NAME_INTERPOLATED_LATITUDE = "InterpolatedLatitude";     // name of the InterpolatedLatitude output column
        private const string OUTPUT_COLUMN_NAME_INTERPOLATED_LONGITUDE = "InterpolatedLongitude";   // name of the InterpolatedLatitude output column
        private const string OUTPUT_COLUMN_NAME_STATUSCODE = "StatusCode";                          // name of the MatchCode output column
        private const string OUTPUT_COLUMN_NAME_CONFIDENCE = "Confidence";                          // Name of the Confidence output column
        private const string OUTPUT_COLUMN_NAME_VALID_FORMATTEDADDRESS = "ValidatedFormattedAddress";   // These columns return validated address info if enabled
        private const string OUTPUT_COLUMN_NAME_VALID_ADDRESSLINE = "ValidatedAddressLine";
        private const string OUTPUT_COLUMN_NAME_VALID_ADMINDISTRICT = "ValidatedAdminDistrict";
        private const string OUTPUT_COLUMN_NAME_VALID_COUNTRYREGION = "ValidatedCountryRegion";
        private const string OUTPUT_COLUMN_NAME_VALID_DISTRICT = "ValidatedDistrict";
        private const string OUTPUT_COLUMN_NAME_VALID_LOCALITY = "ValidatedLocality";
        private const string OUTPUT_COLUMN_NAME_VALID_POSTALCODE = "ValidatedPostalCode";
        private const string OUTPUT_COLUMN_NAME_VALID_POSTALTOWN = "ValidatedPostalTown";
        private const string PROPERTY_NAME_OUTPUT_CODE_PAGE = "Added Output Columns Code Page";
        private const string PROPERTY_NAME_JOB_DESCRIPTION = "Job Description";       // these represent the names of the associated transformation property
        private const string PROPERTY_NAME_BING_MAPS_KEY = "Bing Maps Key";
        private const string PROPERTY_NAME_BING_MIN_CONFIDENCE = "Bing Maps Minimum Confidence";
        private const string PROPERTY_NAME_ADD_KEYS_TO_OUTPUTS = "Add Search Key Columns To Outputs";
        private const string PROPERTY_NAME_ADD_ADDRESS_TO_OUTPUTS = "Add Validated Address Columns To Outputs";
        private const string PROPERTY_NAME_SERVICE_RETRYATTEMPTS = "Bing Maps Request Retry Attempts";    
        private const string PROPERTY_NAME_SERVICE_RETRYDELAY = "Web Request Retry Delay";
        private const string PROPERTY_NAME_SERVICE_STUBDATA = "Service Stub Bing Maps Data";
        //See http://msdn.microsoft.com/en-us/library/ff701736.aspx  for source field definitions
        private const string PROPERTY_NAME_INPUT_ENTITYID = "Source Entity ID";
        private const string PROPERTY_NAME_INPUT_CULTURE = "Source Culture";
        private const string PROPERTY_NAME_INPUT_QUERY = "Source Composite Query";
        private const string PROPERTY_NAME_INPUT_ADDRESSLINE = "Source AddressLine";
        private const string PROPERTY_NAME_INPUT_ADMINDISTRICT = "Source Administrative District";
        private const string PROPERTY_NAME_INPUT_COUNTRYREGION = "Source CountryRegion";
        private const string PROPERTY_NAME_INPUT_DISTRICT = "Source District";
        private const string PROPERTY_NAME_INPUT_LOCALITY = "Source Locality";
        private const string PROPERTY_NAME_INPUT_POSTALCODE = "Source PostalCode";
        private const string PROPERTY_NAME_INPUT_POSTALTOWN = "Source Postal Town";

        /// <summary>
        /// Bing Maps minimum confidence level enum
        /// </summary>
        private enum BingMapsMinConfidence
        {
            Default = 0,    //Omit min confidence level from request
            Low,
            Medium,
            High
        }

        #endregion

        #region Private Member Variables
        private PipelineBuffer outputBuffer;                    // transformation's output buffer for asynchronous output
        private Hashtable bufferInputLineageMap;                // used to store the lineage mapping b/w the input and output buffers
        private Hashtable bufferOutputLineageMap;               // used to store the lineage mapping b/w the input and output buffers
        private Hashtable passThruRows;                         // used to pass thru other input columns of information not directly related to geocoding, but defined in the input buffer
        private Geocoder geocoder;                              // local reference to our geocoder component
        private bool pbCancel = false;                          // Used by FireError
        
        // During PreExecute the transformation's properties will get resolved (from package 
        // variables, input columns, or literals) and stored in the following variables.
        private string sJobDescription = "";                    // Optional job description string
        private string runtimeBingKey = "";                            // Bing Maps key
        private string runtimeBingMinConfidence = "";                  // Minimum confidence for matching
        private bool fRuntimePassSearchKeysToOutputs = false;          // Pass search key input columns to output columns
        private bool fRuntimeAddValidatedAddressToOutputs = false;     // Add validated address fields to output columns
        private int serviceRetryAttempts = 10;                  // number of attempts to retry accessing Bing Maps services
        private int serviceRetryDelay = 5;                      // delay between attempts
        private bool serviceStubBingMapsData = false;           // will cause the transformation to avoid any Web Service calls and synthesis fake lat/lon values instead

        //Names of optional source input fields
        List<string> OptionalSourceColNames = new List<string>();   //Collection containing all optional input column names
        private string sPropInputCulture = null;
        private string sPropInputQuery = null;
        private string sPropInputAddressLine = null;
        private string sPropInputAdminDistrict = null;
        private string sPropInputCountryRegion = null;
        private string sPropInputDistrict = null;
        private string sPropInputLocality = null;
        private string sPropInputPostalCode = null;
        private string sPropInputPostalTown = null;
        #endregion

        /// <summary>
        /// Overrides the ProcessInput method.  See books online for more information.
        /// </summary>
        public override void ProcessInput(int inputID, PipelineBuffer buffer)
        {
            // Check to see if there are rows in the buffer.
            if (!buffer.EndOfRowset)
            {
                // Create a hashtable used to pass thru other columns of information not directly
                // related to geocoding, but defined for the row.
                passThruRows = new Hashtable();

                // Advance the buffer to the next row.
                while (buffer.NextRow())
                {
                    string inputEntityID = GetTrimmedInputValue(buffer, SourceEntityIDColumnName);
                    string inputCulture = GetTrimmedInputValue(buffer, sPropInputCulture);
                    string inputQuery = GetTrimmedInputValue(buffer, sPropInputQuery);
                    string inputAddressLine = GetTrimmedInputValue(buffer, sPropInputAddressLine);
                    string inputAdminDistrict = GetTrimmedInputValue(buffer, sPropInputAdminDistrict);
                    string inputCountryRegion = GetTrimmedInputValue(buffer, sPropInputCountryRegion);
                    string inputDistrict = GetTrimmedInputValue(buffer, sPropInputDistrict);
                    string inputLocality = GetTrimmedInputValue(buffer, sPropInputLocality);
                    string inputPostalCode = GetTrimmedInputValue(buffer, sPropInputPostalCode);
                    string inputPostalTown = GetTrimmedInputValue(buffer, sPropInputPostalTown);

                    // Save a copy of all the passthru values to join back to the 
                    // geocoded data and send to the output
                    Hashtable passThruValues = new Hashtable();

                    foreach (IDTSInputColumn100 col in InCols)
                    {
                        // Don't pass search key columns through unless requested
                        if (fRuntimePassSearchKeysToOutputs || !OptionalSourceColNames.Contains(col.Name))
                        {
                            passThruValues.Add(col.Name, buffer[(int)bufferInputLineageMap[col.Name]]);
                        }
                    }

                    if (passThruValues.ContainsKey(inputEntityID)) 
                    { 
                        throw new ApplicationException("Duplicate EntityID detected.  EntityID must be unique"); 
                    }

                    passThruRows.Add(inputEntityID, passThruValues);

                    // Add the current row to our batch buffer
                    geocoder.AddAddress(inputEntityID, inputCulture, inputQuery, inputAddressLine, inputAdminDistrict, inputCountryRegion, 
                        inputDistrict, inputLocality, inputPostalCode, inputPostalTown, runtimeBingMinConfidence);
                }

                // Flush the current batch (causes the geocoding operation to execute)
                if (geocoder.AddressCount != 0) FlushCurrentBatch();

                passThruRows = null;
            }
            else
            {
                // EndOfRowset on the input buffer is true.
                // Set EndOfRowset on the output buffer.
                outputBuffer.SetEndOfRowset();
            }
        }

        /// <summary>
        /// Processes the batch of addresses currently added to the geocoder component, and adds the geocoded data to the output.
        /// </summary>
        private void FlushCurrentBatch()
        {
            // Use our geocoder utility to perform the geocoding process with Bing Maps.
            geocoder.ExecuteBatch();

            // Read the geocoded data
            while (geocoder.Read())
            {
                // Add a row to the output buffer.
                outputBuffer.AddRow();

                // Add the passthru values back to the output by finding the correct row in the passthru collection
                Hashtable passThruValues = (Hashtable)passThruRows[geocoder.GetEntityID];      

                foreach (IDTSInputColumn100 col in InCols)
                {
                    if (fRuntimePassSearchKeysToOutputs || !OptionalSourceColNames.Contains(col.Name))
                        outputBuffer[(int)bufferOutputLineageMap[col.Name]] = passThruValues[col.Name];
                }

                // Copy geocoder response data to the output buffer columns.
                SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_ENTITY_TYPE, geocoder.GetEntityType, 32);
                SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_STATUSCODE, geocoder.GetStatusCode, 32);
                SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_CONFIDENCE, geocoder.GetConfidence, 32);

                // Output rooftop location if available
                if (!geocoder.GetHasRooftopLocation)
                {
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_ROOFTOP_LATITUDE, null);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_ROOFTOP_LONGITUDE, null);
                }
                else
                {
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_ROOFTOP_LATITUDE, geocoder.GetRooftopLatitude);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_ROOFTOP_LONGITUDE, geocoder.GetRooftopLongitude);
                }
                
                // Output interpolated location if available
                if (!geocoder.GetHasInterpolatedLocation)
                {
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_INTERPOLATED_LATITUDE, null);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_INTERPOLATED_LONGITUDE, null);
                }
                else
                {
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_INTERPOLATED_LATITUDE, geocoder.GetInterpolatedLatitude);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_INTERPOLATED_LONGITUDE, geocoder.GetInterpolatedLongitude);
                }

                if (fRuntimeAddValidatedAddressToOutputs)
                {
                    //Copy validated address data from geocoder response to output buffer if requested
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_VALID_ADDRESSLINE, geocoder.GetAddressLine, 50);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_VALID_ADMINDISTRICT, geocoder.GetAdminDistrict, 50);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_VALID_COUNTRYREGION, geocoder.GetCountryRegion, 50);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_VALID_DISTRICT, geocoder.GetDistrict, 50);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_VALID_FORMATTEDADDRESS, geocoder.GetFormattedAddress, 500);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_VALID_LOCALITY, geocoder.GetLocality, 50);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_VALID_POSTALCODE, geocoder.GetPostalCode, 50);
                    SetOutputBufferColumnValue(outputBuffer, OUTPUT_COLUMN_NAME_VALID_POSTALTOWN, geocoder.GetPostalTown, 50);
                }
            }

            // Reset the geocoder for the next batch.
            geocoder.Reset();
        }

        /// <summary>
        /// Overrides the PreExecute method.  See books online for more information.
        /// </summary>
        public override void PreExecute()
        {
            base.PreExecute();

            // Resolve the Bing Maps settings, some which come from variables.
            //
            IDTSVariables100 vars = null;

            try
            {
                //Job description variable might not be defined because variable is optional
                VariableDispenser.LockForRead(ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_JOB_DESCRIPTION].Value.ToString());
            }
            catch
            {
            }

            VariableDispenser.LockForRead(ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_BING_MAPS_KEY].Value.ToString());

            VariableDispenser.GetVariables(out vars);

            try
            {
                //Get job description if defined
                sJobDescription = vars[ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_JOB_DESCRIPTION].Value].Value.ToString().Trim();
            }
            catch
            {
                sJobDescription = null;  //Don't tag geocode batch with job description if not defined
            }

            //Note that these member values are only available during execution (for speed), not at design time
            runtimeBingKey = vars[ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_BING_MAPS_KEY].Value].Value.ToString().Trim();
            runtimeBingMinConfidence = ((BingMapsMinConfidence)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_BING_MIN_CONFIDENCE].Value).ToString();
            fRuntimePassSearchKeysToOutputs = PassSearchKeysToOutputs;
            fRuntimeAddValidatedAddressToOutputs = AddValidatedAddressToOutputs;
            //serviceRetryAttempts = (int)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_SERVICE_RETRYATTEMPTS].Value;
            //serviceRetryDelay = (int)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_SERVICE_RETRYDELAY].Value;
            //serviceStubBingMapsData = (bool)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_SERVICE_STUBDATA].Value;

            // According to documentation, the indexes of the input columns of the buffer
            // stream may not correspond to the original input columns of the metadata.  You
            // must provide a mapping between the two using FindColumnByLineageID, which is
            // what the code below does.  We will store this mapping in two separate Hashtables,
            // input and output, for lookup purposes later on.
            bufferInputLineageMap = new Hashtable();
            bufferOutputLineageMap = new Hashtable();

            foreach (IDTSInputColumn100 inCol in InCols)
            {
                bufferInputLineageMap.Add(inCol.Name, BufferManager.FindColumnByLineageID(ComponentMetaData.InputCollection[0].Buffer, inCol.LineageID));
            }
            
            foreach (IDTSOutputColumn100 outCol in OutCols)
            {
                bufferOutputLineageMap.Add(outCol.Name, BufferManager.FindColumnByLineageID(ComponentMetaData.OutputCollection[0].Buffer, outCol.LineageID));
            }
            
            // Create the batch geocoder, and initialize
            geocoder = new Geocoder(sJobDescription, runtimeBingKey, serviceRetryAttempts, serviceRetryDelay, serviceStubBingMapsData);
            geocoder.RaiseLogVerboseEvent += HandleGeocoderLogVerboseEvent;
        }

        /// <summary>
        /// Overrides the PrimeOutput method.  See books online for more information.
        /// </summary>
        public override void PrimeOutput(int outputs, int[] outputIDs, PipelineBuffer[] buffers)
        {
            if (buffers.Length != 0) 
                outputBuffer = buffers[0];
        }

        /// <summary>
        /// Overrides the ProvideComponentProperties method.  See books online for more information.
        /// </summary>
        public override void ProvideComponentProperties()
        {
            // Let the base class add the input and output objects.
            base.ProvideComponentProperties();

            // Name the input and output, and make the output asynchronous.
            IDTSOutput100 output = ComponentMetaData.OutputCollection[0];
            IDTSInput100 input = ComponentMetaData.InputCollection[0];
            input.Name = "Input";
            output.Name = "Output";
            output.SynchronousInputID = 0;

            //Set standard defaults for job description and Bing Maps key if default variables are found
            string sJobDescriptionDefault = "";
            string sBingMapsKeyDefault = "";

            if (VariableDispenser.Contains("User::JobDescription"))
                sJobDescriptionDefault = "User::JobDescription";

            if (VariableDispenser.Contains("User::BingMapsKey"))
                sBingMapsKeyDefault = "User::BingMapsKey";

            // Add the Custom properties for the geocoder settings and source mappings
            //
            AddCustomProperty(PROPERTY_NAME_OUTPUT_CODE_PAGE, "The code page to use for added output columns containing string values (0 = Unicode). Note that any output columns that are passed through from input retain their original code page.", 
                0, CustomPropertiesBase.Literal); 
            AddCustomProperty(PROPERTY_NAME_JOB_DESCRIPTION, "The package variable containing the job description for the Bing Maps geocode job.", 
                sJobDescriptionDefault, CustomPropertiesBase.Variables);
            AddCustomProperty(PROPERTY_NAME_BING_MAPS_KEY, "The package variable containing the Bing Maps key.", 
                sBingMapsKeyDefault, CustomPropertiesBase.Variables);
            AddCustomProperty(PROPERTY_NAME_BING_MIN_CONFIDENCE, "The minimum confidence level required for a location request to match successfully.", 
                BingMapsMinConfidence.Default, CustomPropertiesBase.MinConfidence);
            AddCustomProperty(PROPERTY_NAME_ADD_KEYS_TO_OUTPUTS, "If True, all defined search key columns (Query, Culture, AddressLine, etc.) are passed through as outputs", 
                false, CustomPropertiesBase.TrueFalseMetadataChange);
            AddCustomProperty(PROPERTY_NAME_ADD_ADDRESS_TO_OUTPUTS, "If True, all available validated address fields that can be returned by the geocoder are added as outputs", 
                false, CustomPropertiesBase.TrueFalseMetadataChange);
            //AddCustomProperty(PROPERTY_NAME_SERVICE_RETRYATTEMPTS, "Number of attempts to retry accessing Bing Maps services in case of network failure.", 
            //  serviceRetryAttempts, CustomPropertiesBase.Literal);
            //AddCustomProperty(PROPERTY_NAME_SERVICE_RETRYDELAY, "The delay between retries (in seconds).", 
            //  serviceRetryDelay, CustomPropertiesBase.Literal);
            //AddCustomProperty(PROPERTY_NAME_SERVICE_STUBDATA, "If true, will cause the transformation to avoid any Web Service calls and instead will synthesis fake lat/lon values instead of invoking the service. Useful for testing purposes.", 
            //  serviceStubBingMapsData, CustomPropertiesBase.Literal);
            AddCustomProperty(PROPERTY_NAME_INPUT_ENTITYID, "The input column to use as the Bing Maps EntityID.", 
                "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_CULTURE, "The input column to use as the Bing Maps Culture (e.g. en-US).", 
                "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_QUERY, "The input column to use as the Bing Maps Query (composite query string). Overrides all other search key fields if defined and not empty.", 
                "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_ADDRESSLINE, "The input column to use as the Bing Maps AddressLine.", 
                "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_LOCALITY, "The input column to use as the Bing Maps Locality (e.g. City).",
                "", CustomPropertiesBase.InputColumns); 
            AddCustomProperty(PROPERTY_NAME_INPUT_ADMINDISTRICT, "The input column to use as the Bing Maps AdminDistrict (e.g. State).", 
                "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_POSTALCODE, "The input column to use as the Bing Maps PostalCode (e.g. ZIP Code).",
                "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_POSTALTOWN, "The input column to use as the Bing Maps PostalTown (optional).",
                "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_DISTRICT, "The input column to use as the Bing Maps District (optional; not used with all countries).",
                "", CustomPropertiesBase.InputColumns);
            AddCustomProperty(PROPERTY_NAME_INPUT_COUNTRYREGION, "The input column to use as the Bing Maps CountryRegion (e.g. United States).",
                "", CustomPropertiesBase.InputColumns);
        }

        /// <summary>
        /// Called when the component input is attached to an upstream output
        /// </summary>
        /// <param name="inputID"></param>
        public override void OnInputPathAttached(int inputID)
        {
            base.OnInputPathAttached(inputID);

            //Find the virtual input column set
            IDTSInput100 inAtt = ComponentMetaData.InputCollection.FindObjectByID(inputID);
            IDTSVirtualInput100 vi = inAtt.GetVirtualInput();

            //Make all of the input columns read only to start with (the user can then turn off columns as desired)
            foreach (IDTSVirtualInputColumn100 col in vi.VirtualInputColumnCollection)
            {
                vi.SetUsageType(col.LineageID, DTSUsageType.UT_READONLY);
            }

            //Set custom property defaults for properties that exactly match column names
            bool fOutputTypeDefaultSet = false;

            foreach (IDTSInputColumn100 col in InCols)
            {
                bool fIsCandidateOutputTypeDefault = true;

                switch (col.Name)
                {
                    case "EntityID":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_ENTITYID, col.Name);
                        fIsCandidateOutputTypeDefault = false;  //Don't want to use this one to determine output column type
                        break;
                    case "Culture":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_CULTURE, col.Name);
                        break;
                    case "Query":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_QUERY, col.Name);
                        break;
                    case "AddressLine":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_ADDRESSLINE, col.Name);
                        break;
                    case "AdminDistrict":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_ADMINDISTRICT, col.Name);
                        break;
                    case "CountryRegion":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_COUNTRYREGION, col.Name);
                        break;
                    case "District":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_DISTRICT, col.Name);
                        break;
                    case "Locality":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_LOCALITY, col.Name);
                        break;
                    case "PostalCode":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_POSTALCODE, col.Name);
                        break;
                    case "PostalTown":
                        SetCustomPropertyDefault(PROPERTY_NAME_INPUT_POSTALTOWN, col.Name);
                        break;
                }

                //Set the output code page custom property using the code page of the first candidate input column found
                if (fIsCandidateOutputTypeDefault && !fOutputTypeDefaultSet)
                {
                    int iCodePage = 0;

                    switch (col.DataType)
                    {
                        case DataType.DT_STR:
                        case DataType.DT_WSTR:
                            if (col.DataType.Equals(DataType.DT_STR))
                                iCodePage = col.CodePage;

                            ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_OUTPUT_CODE_PAGE].Value = iCodePage;
                            fOutputTypeDefaultSet = true;
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Overrides the ReinitializeMetaData method.  See books online for more information.
        /// </summary>
        public override void ReinitializeMetaData()
        {
            base.ReinitializeMetaData();

            //Clean up any invalid input columns
            if (!ComponentMetaData.AreInputColumnsValid) 
                ComponentMetaData.RemoveInvalidInputColumns();

            List<String> ExpectedOutputCols = new List<string>();   //Build list of expected output columns
            List<int> OutputColsToRemove = new List<int>();   //List of output columns to remove

            // Add the output columns minus query columns, making them identical to the input columns PLUS
            // additional columns for geocode results
            //
            foreach (IDTSInputColumn100 inCol in InCols)
            {
                if (PassSearchKeysToOutputs || !OptionalSourceColNames.Contains(inCol.Name))
                {
                    if (!OutputColumnExists(inCol.Name))
                    {
                        IDTSOutputColumn100 outCol = ComponentMetaData.OutputCollection[0].OutputColumnCollection.New();
                        outCol.Name = inCol.Name;
                        outCol.SetDataTypeProperties(inCol.DataType, inCol.Length, inCol.Precision, inCol.Scale, inCol.CodePage);
                    }

                    ExpectedOutputCols.Add(inCol.Name);
                }
            }

            // Add the geocoder results columns
            CreateNewOutputColumn(OUTPUT_COLUMN_NAME_ENTITY_TYPE, DataType.DT_WSTR, 32);
            ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_ENTITY_TYPE);
            CreateNewOutputColumn(OUTPUT_COLUMN_NAME_STATUSCODE, DataType.DT_WSTR, 32);
            ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_STATUSCODE);
            CreateNewOutputColumn(OUTPUT_COLUMN_NAME_CONFIDENCE, DataType.DT_WSTR, 32);
            ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_CONFIDENCE);
            CreateNewOutputColumn(OUTPUT_COLUMN_NAME_ROOFTOP_LATITUDE, DataType.DT_R8, 0);
            ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_ROOFTOP_LATITUDE);
            CreateNewOutputColumn(OUTPUT_COLUMN_NAME_ROOFTOP_LONGITUDE, DataType.DT_R8, 0);
            ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_ROOFTOP_LONGITUDE);
            CreateNewOutputColumn(OUTPUT_COLUMN_NAME_INTERPOLATED_LATITUDE, DataType.DT_R8, 0);
            ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_INTERPOLATED_LATITUDE);
            CreateNewOutputColumn(OUTPUT_COLUMN_NAME_INTERPOLATED_LONGITUDE, DataType.DT_R8, 0);
            ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_INTERPOLATED_LONGITUDE);

            if (AddValidatedAddressToOutputs)
            {
                //Add the validated address columns if requested
                CreateNewOutputColumn(OUTPUT_COLUMN_NAME_VALID_FORMATTEDADDRESS, DataType.DT_WSTR, 500);
                ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_VALID_FORMATTEDADDRESS);
                CreateNewOutputColumn(OUTPUT_COLUMN_NAME_VALID_ADDRESSLINE, DataType.DT_WSTR, 50);
                ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_VALID_ADDRESSLINE);
                CreateNewOutputColumn(OUTPUT_COLUMN_NAME_VALID_ADMINDISTRICT, DataType.DT_WSTR, 50);
                ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_VALID_ADMINDISTRICT);
                CreateNewOutputColumn(OUTPUT_COLUMN_NAME_VALID_COUNTRYREGION, DataType.DT_WSTR, 50);
                ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_VALID_COUNTRYREGION);
                CreateNewOutputColumn(OUTPUT_COLUMN_NAME_VALID_DISTRICT, DataType.DT_WSTR, 50);
                ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_VALID_DISTRICT);
                CreateNewOutputColumn(OUTPUT_COLUMN_NAME_VALID_LOCALITY, DataType.DT_WSTR, 50);
                ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_VALID_LOCALITY);
                CreateNewOutputColumn(OUTPUT_COLUMN_NAME_VALID_POSTALCODE, DataType.DT_WSTR, 50);
                ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_VALID_POSTALCODE);
                CreateNewOutputColumn(OUTPUT_COLUMN_NAME_VALID_POSTALTOWN, DataType.DT_WSTR, 50);
                ExpectedOutputCols.Add(OUTPUT_COLUMN_NAME_VALID_POSTALTOWN);
            }

            //Remove any other output columns
            foreach (IDTSOutputColumn100 col in OutCols)
            {
                if (!ExpectedOutputCols.Contains(col.Name))
                {
                    OutputColsToRemove.Add(col.ID);
                }
            }

            foreach (int ID in OutputColsToRemove)
            {
                ComponentMetaData.OutputCollection[0].OutputColumnCollection.RemoveObjectByID(ID);
            }

        }

        /// <summary>
        /// Create a new output column with data type and code page that agree with what is selected in the custom properties
        /// </summary>
        /// <param name="name"></param>
        /// <param name="dataType"></param>
        /// <param name="size"></param>
        /// <remarks>If the column already exists and the data type is correct, do nothing.</remarks>
        private void CreateNewOutputColumn(string name, DataType dataType, int size)
        {
            IDTSOutputColumn100 outCol = null;

            //Adjust requested data type and code page according to output code page custom property
            int outCodePage = OutputCodePage;

            if (dataType == DataType.DT_WSTR && outCodePage != 0)
                dataType = DataType.DT_STR;     //Change type to match
            else if (dataType == DataType.DT_STR && outCodePage == 0)
                dataType = DataType.DT_WSTR;    //Change type to match
            else
                outCodePage = 0;                //No code page if not string

            if (OutputColumnExists(name))
            {
                if (InputColumnExists(name))
                {
                    ComponentMetaData.FireError(0, ComponentMetaData.Name, "Added output column " + name + " already exists as an input column", "", 0, out pbCancel);
                    return;
                }

                //Compare existing column data type with required data type
                outCol = OutCols[name];

                if (!outCol.DataType.Equals(dataType) || dataType == DataType.DT_STR && outCol.CodePage != outCodePage)
                {
                    //Column exists and data type is mismatched -- remove it
                    ComponentMetaData.OutputCollection[0].OutputColumnCollection.RemoveObjectByID(outCol.ID);
                    outCol = null;
                }
                else
                {
                    return;     //Output exists and data type is OK
                }
            }

            //Create the new column
            outCol = ComponentMetaData.OutputCollection[0].OutputColumnCollection.New();
            outCol.Name = name;
            outCol.SetDataTypeProperties(dataType, size, 0, 0, outCodePage);
        }

        /// <summary>
        /// Overrides the Validate method.  See books online for more information.
        /// </summary>
        public override DTSValidationStatus Validate()
        {
            DTSValidationStatus status = base.Validate();

            // Validate the required geocoder settings
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_OUTPUT_CODE_PAGE) ||
                !ValidateCustomPropertySpecified(PROPERTY_NAME_BING_MAPS_KEY) ||
                !ValidateCustomPropertySpecified(PROPERTY_NAME_BING_MIN_CONFIDENCE) ||
                !ValidateCustomPropertySpecified(PROPERTY_NAME_ADD_KEYS_TO_OUTPUTS) ||
                !ValidateCustomPropertySpecified(PROPERTY_NAME_ADD_ADDRESS_TO_OUTPUTS)) 
                return DTSValidationStatus.VS_ISBROKEN;
            //if (!ValidateCustomPropertySpecified(PROPERTY_NAME_SERVICE_RETRYATTEMPTS)) return DTSValidationStatus.VS_ISBROKEN;
            //if (!ValidateCustomPropertySpecified(PROPERTY_NAME_SERVICE_RETRYDELAY)) return DTSValidationStatus.VS_ISBROKEN;
            //if (!ValidateCustomPropertySpecified(PROPERTY_NAME_SERVICE_STUBDATA)) return DTSValidationStatus.VS_ISBROKEN;

            if (OutputCodePage < 0 || OutputCodePage > 32767)
            {
                ComponentMetaData.FireError(0, this.ComponentMetaData.Name, "Added output columns code page must be between 0 and 32767", "", 0, out pbCancel);
                return DTSValidationStatus.VS_ISBROKEN;
            }

            // Validate all optional columns to determine availability and save column names
            sPropInputQuery = GetOptionalSourceColName(PROPERTY_NAME_INPUT_QUERY);
            sPropInputAddressLine = GetOptionalSourceColName(PROPERTY_NAME_INPUT_ADDRESSLINE);
            sPropInputAdminDistrict = GetOptionalSourceColName(PROPERTY_NAME_INPUT_ADMINDISTRICT);
            sPropInputCountryRegion = GetOptionalSourceColName(PROPERTY_NAME_INPUT_COUNTRYREGION);
            sPropInputDistrict = GetOptionalSourceColName(PROPERTY_NAME_INPUT_DISTRICT);
            sPropInputLocality = GetOptionalSourceColName(PROPERTY_NAME_INPUT_LOCALITY);
            sPropInputPostalCode = GetOptionalSourceColName(PROPERTY_NAME_INPUT_POSTALCODE);
            sPropInputPostalTown = GetOptionalSourceColName(PROPERTY_NAME_INPUT_POSTALTOWN);
            sPropInputCulture = GetOptionalSourceColName(PROPERTY_NAME_INPUT_CULTURE);

            // Validate that required Source columns have been mapped (must include Entity ID and either a query or some other search key)
            if (!ValidateCustomPropertySpecified(PROPERTY_NAME_INPUT_ENTITYID))
            {
                return DTSValidationStatus.VS_ISBROKEN;
            }

            if (string.IsNullOrEmpty(sPropInputQuery) &&
                 string.IsNullOrEmpty(sPropInputAddressLine) &&
                 string.IsNullOrEmpty(sPropInputAdminDistrict) &&
                 string.IsNullOrEmpty(sPropInputCountryRegion) &&
                 string.IsNullOrEmpty(sPropInputDistrict) &&
                 string.IsNullOrEmpty(sPropInputLocality) &&
                 string.IsNullOrEmpty(sPropInputPostalCode) &&
                 string.IsNullOrEmpty(sPropInputPostalTown))
            {
                ComponentMetaData.FireError(0, ComponentMetaData.Name, "No search key columns have been specified", "", 0, out pbCancel);
                return DTSValidationStatus.VS_ISBROKEN;
            }

            // Verify all input columns are available as output columns, since we should
            // passthru all columns of information even if they're not involved in geocoding.
            foreach (IDTSInputColumn100 inCol in InCols)
            {
                if ((PassSearchKeysToOutputs || !OptionalSourceColNames.Contains(inCol.Name)) && !ValidateOutputColumnExists(inCol))
                {
                    return DTSValidationStatus.VS_NEEDSNEWMETADATA;
                }
            }

            // Verify that the added output columns are available.
            if (!ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_ENTITY_TYPE) ||
                !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_ROOFTOP_LATITUDE) ||
                !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_ROOFTOP_LONGITUDE) ||
                !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_INTERPOLATED_LATITUDE) ||
                !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_INTERPOLATED_LONGITUDE) ||
                !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_STATUSCODE) ||
                !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_CONFIDENCE)) 
            {
                return DTSValidationStatus.VS_NEEDSNEWMETADATA;
            }

            // Verify that the optional valided address columns are available if requested
            if (AddValidatedAddressToOutputs)
            {
                if (!ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_VALID_FORMATTEDADDRESS) ||
                    !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_VALID_ADDRESSLINE) ||
                    !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_VALID_ADMINDISTRICT) ||
                    !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_VALID_COUNTRYREGION) ||
                    !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_VALID_DISTRICT) ||
                    !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_VALID_LOCALITY) ||
                    !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_VALID_POSTALCODE) ||
                    !ValidateOutputColumnExists(OUTPUT_COLUMN_NAME_VALID_POSTALTOWN)) 
                {
                    return DTSValidationStatus.VS_NEEDSNEWMETADATA;
                }
            }

            // If all our own validations succeed, we just return the result
            // of our base classes validation.
            return status;
        }

        /// <summary>
        /// Used internally as a helper function to add custom properties to the transformation. There are three types of properties:
        /// <list type="number">
        ///     <item><term>Literal</term><description>Property whose value is based on a literal.</description></item>
        ///     <item><term>Variable</term><description>Property whose value is based on a variable.</description></item>
        ///     <item><term>InputColumn</term><description>Property whose value is based on an input column.</description></item>
        /// </list>
        /// </summary>
        /// <param name="name">Name of the property to add</param>
        /// <param name="description">Description of the property</param>
        /// <param name="defaultValue">Default value of the property</param>
        /// <param name="propertiesBase">Indicates the type of property</param>
        /// <returns>The added property</returns>
        private IDTSCustomProperty100 AddCustomProperty(string name, string description, object defaultValue, CustomPropertiesBase propertiesBase)
        {
            IDTSCustomProperty100 newProperty = ComponentMetaData.CustomPropertyCollection.New();
            newProperty.Name = name;
            newProperty.Description = description;
            newProperty.Value = defaultValue;

            if (propertiesBase == CustomPropertiesBase.Variables)
            {
                newProperty.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
                newProperty.TypeConverter = typeof(VariablesTypeConverter).AssemblyQualifiedName;
            }
            else if (propertiesBase == CustomPropertiesBase.InputColumns)
            {
                newProperty.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
                newProperty.TypeConverter = typeof(InputColumnsTypeConverter).AssemblyQualifiedName;
            }
            else if (propertiesBase == CustomPropertiesBase.MinConfidence)
            {
                newProperty.ExpressionType = DTSCustomPropertyExpressionType.CPET_NOTIFY;
                newProperty.TypeConverter = typeof(BingMapsMinConfidence).AssemblyQualifiedName;
            }
            else if (propertiesBase == CustomPropertiesBase.TrueFalseMetadataChange)
            {
                //No CPET_NOTIFY because property changes metadata
                newProperty.TypeConverter = typeof(bool).AssemblyQualifiedName;
            }

            return newProperty;
        }

        /// <summary>
        /// Set the value of a custom property, ignoring errors
        /// </summary>
        /// <param name="name"></param>
        /// <param name="defaultValue"></param>
        /// <remarks>Used to initially set default values for enumerated custom properties</remarks>
        private void SetCustomPropertyDefault(string name, object defaultValue)
        {
            try
            {
                ComponentMetaData.CustomPropertyCollection[name].Value = defaultValue;
            }
            catch
            {
            }
        }

        private string DefaultInCol(string colName)
        {
            if (InputColumnExists(colName))
                return colName;

            return "";
        }
        
        /// <summary>
        /// Determines if an output column exists.
        /// </summary>
        /// <param name="columnName">Name of the output column</param>
        /// <returns>True if the column exists</returns>
        private bool InputColumnExists(string columnName)
        {
            try
            {
                // There doesn't seem to be an Exists method, so we simulate one
                // by attempting to reference the column and catching the error.
                IDTSInputColumn100 col = InCols[columnName];
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Determines if an output column exists.
        /// </summary>
        /// <param name="columnName">Name of the output column</param>
        /// <returns>True if the column exists</returns>
        private bool OutputColumnExists(string columnName)
        {
            try
            {
                // There doesn't seem to be an Exists method, so we simulate one
                // by attempting to reference the column and catching the error.
                IDTSOutputColumn100 col = OutCols[columnName];
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// Validate an output column against the corresponding input column
        /// </summary>
        /// <param name="inCol"></param>
        /// <returns></returns>
        private bool ValidateOutputColumnExists(IDTSInputColumn100 inCol)
        {
            string inColName = inCol.Name;

            try
            {
                IDTSOutputColumn100 outCol = OutCols[inColName];

                if (!inCol.DataType.Equals(outCol.DataType) || outCol.DataType.Equals(DataType.DT_STR) && outCol.CodePage != inCol.CodePage)
                {
                    ComponentMetaData.FireError(0, ComponentMetaData.Name, "Output column " + inColName + " differs in type from the corresponding input column", "", 0, out pbCancel);
                    return false;
                }
            }
            catch
            {
                ComponentMetaData.FireError(0, ComponentMetaData.Name, "Output column " + inColName + " does not exist", "", 0, out pbCancel);
                return false;
            }

            return true;
        }

        private bool ValidateOutputColumnExists(string columnName)
        {
            try
            {
                IDTSOutputColumn100 outCol = OutCols[columnName];

                if (outCol.DataType.Equals(DataType.DT_STR) && outCol.CodePage != OutputCodePage ||
                    outCol.DataType.Equals(DataType.DT_WSTR) && outCol.CodePage != 0)
                {
                    ComponentMetaData.FireError(0, ComponentMetaData.Name, "Output column " + columnName + " has the wrong type", "", 0, out pbCancel);
                    return false;
                }

            }
            catch
            {
                ComponentMetaData.FireError(0, ComponentMetaData.Name, "Column " + columnName + " does not exist", "", 0, out pbCancel);
                return false;
            }

            return true;
        }
        
        /// <summary>
        /// Get a string-valued custom property if it is available
        /// </summary>
        /// <param name="propertyName">The name of the custom property</param>
        /// <returns>Returns the trimmed string value, or null if the property is unavailable</returns>
        private string GetTrimedCustomPropertyString(string propertyName)
        {
            string s = null;

            try
            {
                s = ComponentMetaData.CustomPropertyCollection[propertyName].Value.ToString().Trim();
            }
            catch
            {
            }

            if (string.IsNullOrEmpty(s))
                return null;

            return s;
        }

        /// <summary>
        /// Determines if a custom property has been defined in the component's metadata.
        /// </summary>
        /// <param name="propertyName">Name of the custom property</param>
        /// <returns>True if defined</returns>
        private bool ValidateCustomPropertySpecified(string propertyName)
        {
            if (String.IsNullOrEmpty(GetTrimedCustomPropertyString(propertyName)))
            {
                ComponentMetaData.FireError(0, ComponentMetaData.Name, propertyName + " was not specified.", "", 0, out pbCancel);
                return false;
            }
            else
            {
                return true;
            }
        }

        /// <summary>
        /// Return a trimmed input string value if the specified column is valid
        /// </summary>
        /// <param name="buffer">Pipeline buffer</param>
        /// <param name="sColumnName">Column to return</param>
        /// <param name="fColumnIsValid">Validation flag</param>
        /// <returns>Column value or null</returns>
        private string GetTrimmedInputValue(PipelineBuffer buffer, string sColumnName)
        {
            if (!string.IsNullOrEmpty(sColumnName))
            {
                int i = (int)bufferInputLineageMap[sColumnName];

                if (!buffer.IsNull(i))
                {
                    return buffer[i].ToString().Trim();
                }
            }

            return null;
        }

        /// <summary>
        /// Look up the name of an optional source column (search key) from the assigned variable, if there is one.
        /// </summary>
        /// <param name="sPropName"></param>
        /// <returns></returns>
        private string GetOptionalSourceColName(string sPropName)
        {
            string sColName = GetTrimedCustomPropertyString(sPropName);

            if (sColName != null && string.Compare(sColName, "<Blank>", true) == 0) //<Blank> is a placeholder for "no value"
                sColName = null;

            if (sColName != null && !OptionalSourceColNames.Contains(sColName))
                OptionalSourceColNames.Add(sColName);

            return sColName;
        }

        /// <summary>
        /// Set an output buffer column value, which may be null
        /// </summary>
        /// <param name="buffer">The buffer</param>
        /// <param name="colName">The buffer column name</param>
        /// <param name="value">The column value, or null</param>
        private void SetOutputBufferColumnValue(PipelineBuffer buffer, string colName, object value)
        {
            int i = (int)bufferOutputLineageMap[colName];

            if (value == null)
            {
                buffer.SetNull(i);
            }
            else
            {
                buffer[i] = value;
            }
        }

        /// <summary>
        /// Set an output buffer column value, which may be null
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="colName"></param>
        /// <param name="value"></param>
        /// <param name="length"></param>
        /// <remarks>This overload sets a string value and limits it to the specified length</remarks>
        private void SetOutputBufferColumnValue(PipelineBuffer buffer, string colName, string value, int length)
        {
            if (value != null && value.Length > length)
                SetOutputBufferColumnValue(buffer, colName, value.Substring(0, length));
            else
                SetOutputBufferColumnValue(buffer, colName, value);
        }

        /// <summary>
        /// Handles the LogVerbose event of the geocoder in order to log the event
        /// according to the package's log configuration.
        /// </summary>
        void HandleGeocoderLogVerboseEvent(object sender, LogVerboseEventArgs e)
        {
            LogVerbose(e.Message);
        }

        /// <summary>
        /// Logs a specific event to the package's log.
        /// </summary>
        private void LogVerbose(string message)
        {
            byte[] additionalData = null;
            ComponentMetaData.PostLogMessage("OnInformation", this.ComponentMetaData.Name, message, DateTime.Now, DateTime.Now, 0, ref additionalData);
        }

        #region Quick Reference Properties

        /// <summary>Quick reference to the InputCollection</summary>
        IDTSInputColumnCollection100 InCols
        { get { return ComponentMetaData.InputCollection[0].InputColumnCollection; } }

        /// <summary>Quick reference to the OutputCollection</summary>
        IDTSOutputColumnCollection100 OutCols
        { get { return ComponentMetaData.OutputCollection[0].OutputColumnCollection; } }

        /// <summary>Quick reference to the column name of the input's entity id</summary>
        string SourceEntityIDColumnName
        { get { return ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_INPUT_ENTITYID].Value.ToString().Trim(); } }

        /// <summary>Quick reference to the PassSearchKeysToOutputs custom property</summary>
        bool PassSearchKeysToOutputs
        { get { return ((bool)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_ADD_KEYS_TO_OUTPUTS].Value); } }

        /// <summary>Quick reference to the AddValidatedAddressToOutputs custom property</summary>
        bool AddValidatedAddressToOutputs
        { get { return ((bool)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_ADD_ADDRESS_TO_OUTPUTS].Value); } }

        /// <summary>Quick reference to the AddValidatedAddressToOutputs custom property</summary>
        int OutputCodePage
        { get { return ((int)ComponentMetaData.CustomPropertyCollection[PROPERTY_NAME_OUTPUT_CODE_PAGE].Value); } }

        #endregion
    }
}